from channels.generic.websocket import WebsocketConsumer,AsyncWebsocketConsumer
from channels.exceptions import StopConsumer
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from django.core.cache import caches
import time
from datetime import datetime,timedelta
import json
from .logger import info_logger,error_logger

WS_CACHE = caches['redis']
#给channel_name对应的连接发送数据
def send_to_channel_name(channel_name,data,func='forward.message',log_msg=None):
    '''
    :param channel_name: 连接的channel_name
    :param data: 要发送的数据
    :param func: 有消费者类中哪个方法来处理这个数据，方法名自定义，加上是forward_message,传递的应该是formard.message
    :return:
    '''
    # 给指定的channel_name发送消息
    try:
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.send)(
            channel_name,  # 通道名
            {
                'type': func, # 转发给channel_name对应的消费者类下的，func（.转成_）方法来处理这个消息
                'text': data if isinstance(data,str) else json.dumps(data,ensure_ascii=False), #转发给消费者的数据
                'log_msg':log_msg #记录的日志消息
            }
        )
        return True
    except Exception as e:
        return False

#channel_layer通过channel_name主动关闭客户端连接
def server_close_client_by_channel_name(channel_name,log_msg=None):
    try:
        channel_layer = get_channel_layer()
        async_to_sync(channel_layer.send)(
            channel_name,
            {
                "type":"channel.disconnect",
                'log_msg':log_msg
            }
        )
    except Exception as e:
        pass


###1、点对点通信：一个用户只能创建一个连接
class UserToUserConsumer(WebsocketConsumer):
    KEY_LINK = '=*<user-to-user>*=' #缓存到redis时，拼接上的字符串
    Cache = WS_CACHE #缓存到的cahce后端
    Datetime_Format = '%Y-%m-%d %H:%M:%S'
    TIMEOUT=None #缓存到cache的过期，默认是不过期
    EXP = 1 #连接设置的过期时间，分钟
    Logger = info_logger #日志记录方法
    def __init__(self,*args,**kwargs):
        super().__init__(*args,**kwargs)
        #缓存channel_name和过期时间到redis
        self.save_data = {"exp_time":None,"channel_name":None}
        #返回的错误提示的格式
        self.error_data ={"type":"error",'msg':None}
        #删除cache中缓存的数据,self.cache_delete(key)
        self.cache_delete = self.Cache.delete
        #服务端手动断开客户端连接
        self.sever_close = False

    def connect(self):
        # 当前连接所属的用户
        self.username = self.scope['url_route']['kwargs']['username']
        # 日志的前部分消息
        self.log_msg = f'路由={self.scope["path"]},当前用户={self.username},日志内容：'
        # 当前用户的cache_key
        self.cache_key = '{}{}'.format(self.username, self.KEY_LINK)
        # 关闭连接时，code对应的日志消息
        self.code_log = {
            1000: self.log_msg+'是客户端主动断开连接,[code=1000]',
            1006: self.log_msg+f'用户{self.username}已经存在了连接，无法创建新的连接，[code=1006,在方法connect调用close(1006)]',
            3000: self.log_msg+f'用户{self.username}太久没有更新心跳，在发送消息时，被服务端断开连接了,[code=3000,在方法receive调用close(3000)]',
            4000: '用户在转发消息时，发现接收的用户连接已经过期，通过channel_name将其断开，[code=4000,在方法receive使用channel_layer][在channel_disconnect调用close(4000)]',
        }

        #缓存在redis中：channel_name, 过期时间（当前时间+1分钟，在心跳处理时，重新设置过期时间）
        cache_dic = self.Cache.get(self.cache_key)
        #当前时间
        now_time = datetime.now().strftime(self.Datetime_Format)
        self.save_data['channel_name'] = self.channel_name
        if cache_dic:
            exp_time = cache_dic.get('exp_time')
            channel_name = cache_dic.get('channel_name')
            if now_time>exp_time:
                #已经过期了,该用户可以创建新的连接了
                ##1、服务端通过channel_name断开其连接
                self.error_data['msg'] = '太久没有更新心跳，现断开您的连接了'
                # 断开“用户”的连接前，告知断开的原因
                send_to_channel_name(channel_name,json.dumps(self.error_data,ensure_ascii=False))
                #调用断开连接的方法
                log_msg = self.log_msg+f'用户={self.username}开启新连接时，发现之前存在的连接已经过期，对前连接进行关闭'
                server_close_client_by_channel_name(channel_name,log_msg=log_msg)
                ##2、允许客户连接
                self.accept()
                ##3、缓存当前用户设置的过期时间和channel_name
                self.save_data['exp_time'] = (datetime.now()+timedelta(minutes=self.EXP)).strftime(self.Datetime_Format)
                self.Cache.set(self.cache_key,self.save_data,timeout=self.TIMEOUT)
                #日志记录
                log_msg = self.log_msg+f'用户{self.username} 创建连接成功,(之前的连接过期)'
                self.Logger(log_msg)##
            else:
                #channel_name未过期
                #服务端主动断开连接的，不能清除cache中的channel_name数据，还有在运行中的连接
                self.close(1006) #在connect中调用close，code自动设置未1006
                #不能删除cache中的数据，连接正常运行中
        else:
            ###redis中没有缓存当前用户的channel_name
            ##1、允许客户连接
            self.accept()
            ##2、缓存当前用户设置的过期时间和channel_name
            self.save_data['exp_time'] = (datetime.now() + timedelta(minutes=self.EXP)).strftime(self.Datetime_Format)
            self.Cache.set(self.cache_key, self.save_data, timeout=self.TIMEOUT)
            #日志记录
            log_msg = self.log_msg + f'\n用户{self.username} 创建连接成功,(cache中没有数据)'
            self.Logger(log_msg)

    def receive(self, text_data=None, bytes_data=None):
        ###1、判断当前用户的连接是否过期，（更新过期时间，只能在心跳处理中更新）
        cache_data = self.Cache.get(self.cache_key)
        if cache_data:
            exp_time = cache_data.get('exp_time')
            now_time = datetime.now().strftime(self.Datetime_Format)
            if now_time>exp_time:
                self.error_data['msg']='太久没有更新心跳，现断开您的连接了'
                #断开连接前，提示原因
                self.send(json.dumps(self.error_data,ensure_ascii=False))
                # 先断开当前连接
                self.close(3000)
                # 删除当前连接缓存在redis中的数据
                self.cache_delete(self.cache_key)
                return
        else:
            ## 连接正常运行，手动操作redis数据库删除cache_key对应的数据，就会触发这个
            self.error_data['msg'] = '太久没有更新心跳，现断开您的连接了'
            log_msg = self.log_msg +f'\n用户{self.username},连接正常运行时，cache中缓存的channel_name数据被删除掉了[可能是手动删除cache后端存储中的数据了]'
            self.Logger(log_msg)

        ###2、心跳维持
        if text_data in ('ping','PING'):
            #接收的心跳时，重新设置过期时间
            self.save_data['exp_time'] = (datetime.now() + timedelta(minutes=self.EXP)).strftime(self.Datetime_Format)
            self.save_data['channel_name'] = self.channel_name
            self.Cache.set(self.cache_key, self.save_data, timeout=self.TIMEOUT)
            #返回心跳消息
            self.send('PONG')
            # 日志记录
            log_msg =self.log_msg+f'用户{self.username}的心跳'
            self.Logger(log_msg)
            return
        else:
            try:
                dic = json.loads(text_data)
                if not isinstance(dic,dict):
                    self.error_data['msg'] = '请发送{"to":"用户","type":"","data":{}}格式的数据'
                    self.send(json.dumps(self.error_data,ensure_ascii=False))
                    # 日志记录
                    log_msg = self.log_msg+f'\n用户{self.username}发送的数据格式有问题，格式不能是={text_data}'
                    self.Logger(log_msg)
                    return
            except Exception:
                self.error_data['msg'] = '请发送{"to":"用户","type":"","data":{}}格式的数据'
                self.send(json.dumps(self.error_data,ensure_ascii=False))
                # 日志记录
                log_msg = self.log_msg + f'\n用户{self.username}发送的数据格式有问题，格式不能是={text_data}'
                self.Logger(log_msg)
                return

        ###3、转发的数据处理
        msg_type = dic.get('type')
        if msg_type == 'send':
            #转发给哪个用户
            to_username = dic.get('to')
            #转发给该用户的数据
            to_data = dic.get('data')
            #获取要转给的用户的的channel_name
            to_cache_key = '{}{}'.format(to_username,self.KEY_LINK)
            #获取接收用户的channel_name和exp_time
            recv_cache_data = self.Cache.get(to_cache_key)
            #存在该接收用户，就转发
            if recv_cache_data:
                now_time = datetime.now().strftime(self.Datetime_Format)
                exp_time = recv_cache_data.get('exp_time')
                to_channel_name = recv_cache_data.get('channel_name')
                if now_time>exp_time:
                    ##1、接收的用户，连接已经过期,close(4000)
                    self.error_data['msg'] = '太久没有更新心跳，现断开您的连接了'
                    # 断开“接收用户”的连接前，告知断开的原因
                    send_to_channel_name(to_channel_name, json.dumps(self.error_data, ensure_ascii=False))
                    # 断开“接收用户”的连接：触发channel_disconnect方法
                    log_msg = self.log_msg+f'\n{self.username}转发数据给{to_username}时，用户{to_username}连接已经过期，通过channel_name将其断开，[code=4000,在方法receive,转发给用户]'
                    server_close_client_by_channel_name(to_channel_name,log_msg=log_msg)
                    # 删除掉接收用户缓存到cache中的数据
                    self.cache_delete(to_cache_key)

                    ##2、告知发送消息的用户，消息发送失败
                    self.error_data['msg'] = '{}不在线，无法发送'.format(to_username)
                    self.send(json.dumps(self.error_data,ensure_ascii=False))
                else:
                    ##2、开始转发数据
                    # 转发数据
                    send_data = {"type":"recv","from":self.cache_key.split(self.KEY_LINK)[0],"data":to_data}
                    # 日志记录
                    log_msg = self.log_msg + f'\n用户{self.username}转发给{to_username}成功，\n转发数据：{text_data}'
                    # 调用转发
                    send_to_channel_name(to_channel_name,json.dumps(send_data,ensure_ascii=False),log_msg=log_msg)

            ###4、接收用户不存在，无法转发消息
            else:
                self.error_data['msg'] = '{}不在线，无法发送'.format(to_username)
                self.send(json.dumps(self.error_data,ensure_ascii=False))
                # 日志记录
                log_msg = self.log_msg + f'\n用户{self.username}转发给{to_username}失败，{to_username}用户不在线，\n转发数据：{text_data}'
                self.Logger(log_msg)

    def forward_message(self,*args):
        '''
        自定义方法，用户之间实现数据转发，连接对象外部调用此方法给此连接发送消息
        :param args:
        (
            {
                'type': 'forward.message', #别的连接通过另一个连接的channel_name调用的方法
                'text': '{"type": "recv", "from": "lhz", "data": {"msg": "good!"}}' #传递的消息
                'log_msg':'日志内容' #记录的日志内容
            },

        )
        :return:
        '''
        data = args[0]
        text_data = data.get('text','')
        log_msg = data.get('log_msg')
        #在对象外，channel_layer通过channel_name 给对应的websocket发送消息
        json_data = text_data if isinstance(text_data,str) else json.dumps(text_data,ensure_ascii=False)
        self.send(json_data)
        #日志记录
        if log_msg:
            log_msg+=' [forward_message方法实现转发]'
            self.Logger(log_msg)

    # 使用channel_layer关闭连接
    def channel_disconnect(self, *args):
        '''
        消费者对象外部通过channel_name,关闭此客户端连接
        :param args:({'type': 'channel.disconnect', 'log_msg': '日志消息'},)
        :return:
        '''
        data = args[0]
        log_msg = data.get('log_msg', '')
        # 1、在对象外，给channel_layer使用,通过channel_name 来关闭连接
        # 2、close会触发disconnect方法

        # 清除cache中缓存的channel_name数据
        self.cache_delete(self.cache_key)
        # 记录日志
        if log_msg:
            log_msg += ' [在channel_disconnect调用close(4000)]'
            self.code_log[4000] = log_msg
        ##调用close会触发 disconnect方法
        self.close(4000)

    def disconnect(self, code):
        '''
        连接断开时，会触发该方法，在这里主要记录日志消息
        :param code:
            1000=客户端主动断开
            1006=在connect方法中调用close(1006)
            3000=在channel_disconnect方法中调用close(3000)
            3001=在receive方法中，当前连接发送消息时，发现当前连接过期，调用close(3001)
        :return:
        '''
        # 1、在connect方法中，没有调用accept时，就会调用
        # 2、在对象中，调用close方法，就会调用该方法断开当前连接
        ##删除当前连接的缓存的channel_name数

        log_msg = self.code_log.get(code,f'服务端主动关闭{self.username}的连接')
        if code == 1000:
            # 客户端主动关闭
            self.Logger(log_msg)
            # 客户端主动关闭时，要清空掉cache中缓存的channel_name,不然在1分钟内无法创建连接
            self.cache_delete(self.cache_key)
        elif code == 1006:
            #在connect时调用close时触发，不能删除cache中channel_name数据
            self.Logger(log_msg)
        else:
            # 服务器断开连接日志记录，需要删除cahche中的channel_name数据（不在这里操作）
            self.Logger(log_msg)



        ###2、群聊广播通信：一个用户只能创建一个连接

###2、群聊通信：一个用户只能创建一个连接
class GroupChatConsumer(WebsocketConsumer):
    KEY_LINK = '=*<group-chat>*='  # 缓存到redis时，拼接上的字符串
    Cache = WS_CACHE  # 缓存到的cahce后端
    Datetime_Format = '%Y-%m-%d %H:%M:%S'
    TIMEOUT = None  # 缓存到cache的过期，默认是不过期
    EXP = 1  # 连接设置的过期时间，分钟
    Logger = info_logger #日志记录

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 缓存channel_name和过期时间到redis
        self.save_data = {"exp_time": None, "channel_name": None}
        # 返回的错误提示的格式
        self.error_data = {"type": "error", 'msg': None}
        # 删除cache中缓存的数据,self.cache_delete(key)
        self.cache_delete = self.Cache.delete
        # 服务端主动断开连接的吗
    def connect(self):
        ###1、创建基本需要的数据
        # 当前连接所属的组
        self.group = self.scope['url_route']['kwargs']['group']
        # 当前连接所属的用户
        self.username = self.scope['url_route']['kwargs']['username']
        # 日志的前部分消息
        self.log_msg = f'<群聊通信日志> 路由={self.scope["path"]},当前组={self.group},当前用户={self.username},日志内容：'
        # 当前用户的cache_key
        self.cache_key = '{}-{}-{}'.format(self.group,self.KEY_LINK,self.username)
        cache_data = self.Cache.get(self.cache_key)
        now_time =datetime.now().strftime(self.Datetime_Format)
        if cache_data:
            ###2、判断当前用户的连接是否过期了
            exp_time = cache_data.get('exp_time')
            old_channel_name = cache_data.get('channel_name')
            if now_time > exp_time:
                #连接已经过期，通过channel_layer关闭之前的连接，创建新的连接
                send_to_channel_name(old_channel_name,'太久没有更新心跳数据，将关闭连接')
                server_close_client_by_channel_name(old_channel_name)
                #日志记录
                log_msg = self.log_msg+f'组={self.group}，用户={self.username},太久没有更新连接，通过channel_name关闭连接'
                self.Logger(log_msg)

                #允许当前创建连接请求
                self.accept()
                # 加入群聊
                async_to_sync(self.channel_layer.group_add)(self.group, self.channel_name)
                # 将当前连接的channel_name 存到redis中
                self.Cache.set(self.cache_key, self.channel_name, timeout=self.TIMEOUT)
                # 日志记录
                log_msg = self.log_msg + f'用户{self.username}加入群聊{self.group}成功'
                self.Logger(log_msg)
            else:
                #用户之前的连接未过期，无法创建新的连接
                #不调用accept，就会自动触发self.close()方法
                #日志记录
                log_msg = self.log_msg+f'组{self.group},下用户{self.username}，存在运行中的连接，无法创建新的连接'
                self.Logger(log_msg)
                return
        else:
            ###3、当前用户没有还运行中的连接，允许当前的创建连接请求
            #将组内该用户对应的channel_name存到redis中
            self.accept()
            #加入群聊
            async_to_sync(self.channel_layer.group_add)(self.group, self.channel_name)
            #将当前连接的channel_name 存到redis中
            self.Cache.set(self.cache_key,self.channel_name,timeout=self.TIMEOUT)
            #日志记录
            log_msg = self.log_msg+f'用户{self.username}加入群聊{self.group}成功'
            self.Logger(log_msg)

    def disconnect(self, code):
        # 退出群聊
        async_to_sync(self.channel_layer.group_discard)(self.group, self.channel_name)
        # 关闭连接
        self.close()
        #删除redis缓存的channel_name
        self.Cache.delete(self.cache_key)


    def channel_disconnect(self,*args):
        #1、在对象外，给channel_layer使用,通过channel_name 来关闭连接
        #2、close会触发disconnect方法
        # 退出群聊
        async_to_sync(self.channel_layer.group_discard)(self.group, self.channel_name)
        # 关闭连接
        self.close()
        # 删除缓存的channel_name数据
        self.Cache.delete(self.cache_key)
        log_msg = self.log_msg+f'组={self.group}，用户={self.username}，太久没有更新心跳，服务端通过channel_layer关闭连接'
        self.Logger(log_msg)

    def receive(self, text_data=None, bytes_data=None):
        error = {"type":"error","msg":None}
        try:
            dic_data = json.loads(text_data)
            if not isinstance(dic_data,dict):
                error['msg'] = '数据格式以{"type":"send","data":{},"group":"xxx"}'
                self.send(json.dumps(error, ensure_ascii=False))
        except Exception:
            error['msg']='数据格式以{"type":"send","data":{},"group":"xxx"}'
            self.send(json.dumps(error,ensure_ascii=False))
            return

        msg_type = dic_data.get('type')
        if msg_type == 'send':
            send_data = {"type":"group","from":self.username,"data":dic_data.get('data')}
            #给通道组发送数据
            async_to_sync(self.channel_layer.group_send)(
                self.group,
                {
                    "type": "group.message",
                    "text": json.dumps(send_data,ensure_ascii=False),
                },
            )

    def group_message(self,event):
        text_data = event.get('text')
        send_data = text_data if isinstance(text_data,str) else json.dumps(text_data,ensure_ascii=False)
        self.send(send_data)

###3、前端实时更新数据：前端维护一个websocket连接
class TotalDataConsumer(WebsocketConsumer):
    GROUP = 'total'
    def connect(self):
        headers = self.scope["headers"]
        #允许建立连接
        self.accept()
        #将当前通道加入到组内
        async_to_sync(self.channel_layer.group_add)(
            self.GROUP,self.channel_name
        )

    def disconnect(self, code):
        #退出通道组
        async_to_sync(self.channel_layer.group_discard)(
            self.GROUP,
            self.channel_name
        )
        #断开连接
        self.close()

    def receive(self, text_data=None, bytes_data=None):
        dic_data = json.loads(text_data)
        the_type = dic_data.get('type')
        if the_type == 'user_info':
            pass
        elif the_type == 'alert_info':
            pass

    def update_message(self,event):
        #给channel_name 发送的消息时,可以选择触发该方法来实现
        data = event.get('data')
        text_data = data if isinstance(data,str) else json.dumps(data,ensure_ascii=False)
        #给websocket连接发送数据
        self.send(text_data)

